Promises, Async/Await, and Error Handling in JavaScript for React Native
Course: React Native Training
Topic: Promises, Async/Await, and Error Handling
Course Link: React Native Training at JustAcademy
Demo Link: Register for a Course Demo
Website: JustAcademy Official Website
1. Introduction to Asynchronous JavaScript
JavaScript is widely used in React Native to build mobile applications that communicate with servers, retrieve data from APIs, upload files, authenticate users, and save information. Many of these operations take time to complete, so applications need a way to handle them without blocking other work unnecessarily.
Asynchronous programming allows an application to start an operation and continue handling other work while waiting for that operation to finish. When the result becomes available, the application can process the result or handle an error.
Three important concepts used for asynchronous programming in JavaScript are:
- Promises: Objects representing the eventual success or failure of an operation.
- Async/Await: Syntax that makes promise-based code easier to read and write.
- Error Handling: Techniques for detecting, reporting, and recovering from failures.
1.1 What Is Asynchronous Programming?
Asynchronous programming is a programming approach in which an operation can complete later without requiring the surrounding code to wait synchronously for the entire operation to finish.
For example, a React Native application may request a list of products from a server. The request may take a few milliseconds or several seconds, depending on network conditions and server performance. The application can show a loading indicator while the request is in progress and display the products when the response arrives.
1.2 Synchronous vs. Asynchronous Code
| Feature |
Synchronous Code |
Asynchronous Code |
| Execution |
Operations generally run one after another. |
Operations can complete later while other work continues. |
| Waiting |
A long-running operation can delay subsequent code. |
Promise-based operations allow the application to continue other work. |
| Examples |
Simple calculations and variable assignments. |
Network requests, timers, and many file or storage operations. |
| Error handling |
Often uses try...catch for synchronous exceptions. |
Uses promise rejection handlers and/or try...catch with await. |
| React Native use |
Calculating a displayed value. |
Fetching API data or saving information to storage. |
1.3 Synchronous Example
console.log("Step 1");
const result = 10 + 20;
console.log(result);
console.log("Step 3");
Output:
Step 1
30
Step 3
Each statement runs in sequence.
1.4 Asynchronous Example
console.log("Start");
setTimeout(() => {
console.log("Timer completed");
}, 1000);
console.log("Continue running");
Typical output:
Start
Continue running
Timer completed
The timer callback runs later. This demonstrates asynchronous scheduling, although setTimeout() itself does not return a Promise.
2. Learning Objectives
After completing these notes, learners should be able to:
- Explain asynchronous programming and why it is useful in React Native.
- Understand what a JavaScript Promise represents.
- Identify the three main Promise states.
- Create and consume Promises.
- Use
then(), catch(), and finally().
- Understand the purpose of the
async and await keywords.
- Handle errors using
try...catch.
- Fetch and process API data in a React Native application.
- Understand network errors and HTTP response errors.
- Manage loading, success, empty, and error states.
- Execute asynchronous operations sequentially or concurrently.
- Recognize common mistakes and apply best practices.
3. Understanding JavaScript Promises
A Promise is a JavaScript object that represents the eventual completion or failure of an asynchronous operation. It allows code to register handlers that will run when the operation succeeds or fails.
For example, the fetch() function returns a Promise that can be used to process a network response or handle a request failure.
3.1 Why Are Promises Needed?
Before Promises became common, asynchronous code often relied on callbacks. When many asynchronous operations depended on one another, callbacks could become deeply nested and difficult to maintain.
Promises provide a consistent way to chain asynchronous operations, return results from one stage to another, and handle failures through rejection handlers.
3.2 The Three States of a Promise
| State |
Description |
Example |
| Pending |
The operation has not yet completed. |
A network request is still in progress. |
| Fulfilled |
The operation has completed successfully. |
The server returned a usable response. |
| Rejected |
The operation failed. |
The request could not be completed because of a network error. |
A Promise settles when it becomes fulfilled or rejected. Once settled, its state and outcome do not change.
3.3 Basic Promise Example
const myPromise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("Operation completed successfully");
} else {
reject(new Error("Operation failed"));
}
});
myPromise
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error.message);
});
Output when success is true:
Operation completed successfully
The Promise constructor receives an executor function. The resolve() function fulfills the Promise, while reject() rejects it.
3.4 Understanding resolve() and reject()
resolve(value) fulfills the Promise with a value, or adopts the state of another Promise or thenable passed to it.
reject(reason) rejects the Promise with the supplied reason, commonly an Error object.
const resultPromise = new Promise((resolve, reject) => {
const score = 85;
if (score >= 50) {
resolve("Passed");
} else {
reject(new Error("Failed"));
}
});
resultPromise
.then(result => console.log(result))
.catch(error => console.error(error.message));
Output:
Passed
3.5 When Should You Create a Promise?
Most application code consumes Promises returned by APIs such as fetch() or asynchronous libraries. Creating a Promise manually is useful when you need to adapt an asynchronous operation that does not already return a Promise or define a custom asynchronous workflow.
function checkAvailability(isAvailable) {
return new Promise((resolve, reject) => {
if (isAvailable) {
resolve("Item is available");
} else {
reject(new Error("Item is unavailable"));
}
});
}
checkAvailability(true)
.then(message => console.log(message))
.catch(error => console.error(error.message));
In real applications, avoid wrapping an operation in a new Promise when the operation already returns a suitable Promise. Unnecessary wrappers can make error handling more complicated.
4. Handling Promises with then()
The then() method registers a handler for a fulfilled Promise. It can also accept a second callback to handle rejection, although using catch() is often clearer for error handling.
4.1 Basic then() Syntax
promise.then(
result => {
// Handle success.
},
error => {
// Optionally handle rejection.
}
);
4.2 Example: Processing a Successful Promise
const promise = Promise.resolve("Welcome to React Native");
promise.then(message => {
console.log(message);
});
Output:
Welcome to React Native
4.3 Example: Returning a Value from then()
Promise.resolve(10)
.then(number => number * 2)
.then(result => result + 5)
.then(finalResult => {
console.log(finalResult);
});
Output:
25
Each then() call returns a new Promise. A value returned by one handler becomes the fulfillment value for the next handler.
5. Handling Errors with catch()
The catch() method registers a rejection handler. It is commonly placed at the end of a Promise chain so that errors from earlier asynchronous operations can be handled in one place.
5.1 Basic catch() Syntax
somePromise
.then(result => {
console.log(result);
})
.catch(error => {
console.error("Something went wrong:", error.message);
});
5.2 Example: Handling a Rejected Promise
const promise = Promise.reject(new Error("Unable to load data"));
promise
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error.message);
});
Output:
Unable to load data
5.3 Why Should Errors Be Handled?
- To prevent unhandled Promise rejections.
- To show helpful messages to users.
- To provide fallback behavior where appropriate.
- To record useful debugging information.
- To distinguish failed requests from successful responses.
- To keep the application from displaying misleading or incomplete data.
6. The finally() Method
The finally() method registers a callback that runs when a Promise settles, whether it is fulfilled or rejected. It is useful for cleanup tasks that must happen regardless of the result.
6.1 Basic Syntax
promise
.then(result => {
// Handle success.
})
.catch(error => {
// Handle failure.
})
.finally(() => {
// Perform cleanup.
});
6.2 Example: Finishing an Operation
Promise.resolve("Data loaded")
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error.message);
})
.finally(() => {
console.log("Operation finished");
});
Output:
Data loaded
Operation finished
In a React Native screen, similar cleanup logic is often used to stop a loading indicator. The component must still be mounted or otherwise safely handle state updates when the asynchronous operation finishes.
7. Promise Chaining
Promise chaining allows multiple asynchronous operations to be connected in sequence. Each stage can use the result of the previous stage.
7.1 Example of Promise Chaining
function getUserId() {
return Promise.resolve(101);
}
function getUserName(userId) {
return Promise.resolve(`User ${userId}`);
}
getUserId()
.then(userId => getUserName(userId))
.then(userName => {
console.log(userName);
})
.catch(error => {
console.error(error.message);
});
Output:
User 101
7.2 Why Must You Return a Promise?
When an asynchronous operation is started inside a then() callback, return its Promise if the next stage needs to wait for it. Otherwise, the outer chain may continue before the nested operation finishes.
function getData() {
return fetch("https://jsonplaceholder.typicode.com/users")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
});
}
getData()
.then(users => {
console.log(users.length);
})
.catch(error => {
console.error("Request failed:", error.message);
});
The Promise returned by getData() represents the complete operation, including reading the JSON response. Returning the relevant Promises keeps the chain connected and allows failures to reach the final error handler.
8. Introduction to Async/Await
async and await are JavaScript keywords that simplify working with Promises. They make asynchronous code easier to follow by allowing promise-based operations to be written in a style that resembles sequential code.
8.1 What Is an Async Function?
An asynchronous function is declared using the async keyword. Calling an async function always returns a Promise.
async function greet() {
return "Hello, React Native";
}
greet().then(message => {
console.log(message);
});
Output:
Hello, React Native
Although the function returns a string in its body, the caller receives a Promise fulfilled with that string.
8.2 Async Arrow Function
const getMessage = async () => {
return "Welcome";
};
getMessage().then(message => console.log(message));
Arrow functions can also be asynchronous by placing async before their parameter list.
8.3 What Does await Do?
The await keyword pauses the execution of the surrounding async function until the supplied Promise settles. If the Promise fulfills, await evaluates to its fulfillment value. If the Promise rejects, the rejection is thrown as an exception that can be handled with try...catch.
async function getNumber() {
const promise = Promise.resolve(100);
const result = await promise;
console.log(result);
}
getNumber();
Output:
100
await does not freeze the entire JavaScript application. It suspends the current async function while the Promise is pending, allowing other scheduled work to proceed.
8.4 Basic Async/Await Syntax
async function functionName() {
const result = await somePromise;
return result;
}
In general, use await when you need the result of an asynchronous operation before continuing to the next dependent step.
9. Difference Between Promises and Async/Await
| Feature |
Promise Chaining |
Async/Await |
| Syntax |
Uses methods such as then() and catch(). |
Uses async, await, and often try...catch. |
| Readability |
Useful for concise chains and composition. |
Often easier to read for multiple dependent steps. |
| Error handling |
Usually uses catch(). |
Usually uses try...catch. |
| Return type |
Promise methods return new Promises. |
Async functions always return a Promise. |
| Execution |
Handlers run after the corresponding Promise settles. |
await suspends the current async function until settlement. |
| Use cases |
Chaining and composing Promise-based operations. |
Sequential workflows and readable asynchronous functions. |
9.1 The Same Operation Using Promises
function getUser() {
return fetch("https://jsonplaceholder.typicode.com/users/1")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then(user => {
console.log(user.name);
return user;
})
.catch(error => {
console.error("Unable to load user:", error.message);
throw error;
});
}
getUser().catch(() => {
// The caller can handle the propagated error here.
});
9.2 The Same Operation Using Async/Await
async function getUser() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users/1"
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const user = await response.json();
console.log(user.name);
return user;
} catch (error) {
console.error("Unable to load user:", error.message);
throw error;
}
}
getUser().catch(() => {
// The caller can handle the propagated error here.
});
Both approaches can handle the same workflow. Async/Await often makes sequential steps easier to read, while Promise chaining remains useful when composing asynchronous operations.
10. Error Handling with try...catch
The try...catch statement handles exceptions in JavaScript. When used with await, it can handle rejected Promises as well as synchronous errors thrown within the try block.
10.1 Basic Syntax
try {
// Code that might throw an error.
} catch (error) {
// Handle the error.
}
10.2 Example: Catching an Error
try {
throw new Error("Something went wrong");
} catch (error) {
console.log("Error:", error.message);
}
Output:
Error: Something went wrong
10.3 Using try...catch with Await
async function loadData() {
try {
const result = await Promise.reject(
new Error("Unable to load data")
);
console.log(result);
} catch (error) {
console.error(error.message);
}
}
loadData();
Output:
Unable to load data
The rejected Promise causes execution to enter the catch block.
10.4 Using finally with try...catch
async function processData() {
try {
console.log("Processing data");
throw new Error("Processing failed");
} catch (error) {
console.error(error.message);
} finally {
console.log("Cleanup completed");
}
}
processData();
The finally block runs whether the try block succeeds or an error is caught. It is useful for cleanup tasks that must happen regardless of the outcome.
11. Understanding Common JavaScript Errors
React Native applications may encounter different kinds of errors. Recognizing the type of failure makes it easier to select an appropriate recovery strategy.
| Error Type |
Description |
Example |
| Network error |
A request cannot be completed due to a connection or transport problem. |
The device is offline. |
| HTTP error |
The server responds with an unsuccessful HTTP status. |
404 Not Found or 500 Internal Server Error. |
| Parsing error |
Response content cannot be parsed in the expected format. |
Invalid JSON response. |
| Validation error |
Input does not meet application requirements. |
An email address is missing. |
| Authentication error |
The server refuses access because authentication is missing or invalid. |
An expired login session. |
| Programming error |
The code makes an invalid assumption or performs an unsupported operation. |
Accessing a property on undefined. |
11.1 Understanding the Error Object
An Error object commonly includes a message describing the issue and a stack showing where the error originated.
try {
throw new Error("Unable to save profile");
} catch (error) {
console.log(error.name);
console.log(error.message);
console.log(error.stack);
}
Use error messages to help diagnose problems. Avoid exposing internal stack traces, secrets, access tokens, or other sensitive technical details to end users.
11.2 Creating Custom Errors
class ApiError extends Error {
constructor(message, status) {
super(message);
this.name = "ApiError";
this.status = status;
}
}
const error = new ApiError("User not found", 404);
console.log(error.name);
console.log(error.message);
console.log(error.status);
Custom error classes can make application errors easier to classify. For simpler projects, a standard Error object with additional properties may be sufficient.
12. Fetch API and HTTP Error Handling
The Fetch API is commonly used to send HTTP requests in React Native. It returns a Promise that fulfills with a response when the request receives a response. An important detail is that HTTP status codes such as 404 and 500 do not normally cause the Fetch Promise to reject automatically.
Therefore, check the response's ok property or status code before treating the request as successful.
12.1 Basic Fetch Example
async function fetchUsers() {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const users = await response.json();
return users;
}
fetchUsers()
.then(users => console.log(users))
.catch(error => console.error(error.message));
12.2 Why Check response.ok?
The response.ok property is true when the HTTP status is in the successful 200–299 range. If the server returns an unsuccessful status, the code can throw an error to stop the normal success path.
async function getProduct() {
const response = await fetch(
"https://example.com/api/products/1"
);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
}
Replace the example URL with a valid endpoint from your application. A successful HTTP response does not guarantee that the returned content has the exact structure your application expects, so validate important data when necessary.
12.3 Handling JSON Parsing Errors
async function loadProducts() {
try {
const response = await fetch(
"https://example.com/api/products"
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const products = await response.json();
if (!Array.isArray(products)) {
throw new Error("Unexpected product data format");
}
return products;
} catch (error) {
console.error("Unable to load products:", error.message);
throw error;
}
}
If the response body contains invalid JSON, response.json() can reject. The code also checks that the result is an array before returning it.
13. Managing Loading, Success, and Error States in React Native
When an application retrieves data from an API, the screen should communicate the current state to the user. Common states include loading, success, empty results, and failure.
- Loading: The request is in progress.
- Success: The data has been retrieved and is ready to display.
- Empty: The request succeeded, but no items were returned.
- Error: The request failed or the response could not be processed.
13.1 Example: User List with Async/Await
import React, { useEffect, useState } from "react";
import { View, Text, ActivityIndicator, FlatList } from "react-native";
export default function UserListScreen() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState("");
useEffect(() => {
let isActive = true;
async function loadUsers() {
try {
setLoading(true);
setErrorMessage("");
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error("Unexpected response format");
}
if (isActive) {
setUsers(data);
}
} catch (error) {
if (isActive) {
setErrorMessage(
error instanceof Error
? error.message
: "An unexpected error occurred"
);
}
} finally {
if (isActive) {
setLoading(false);
}
}
}
loadUsers();
return () => {
isActive = false;
};
}, []);
if (loading) {
return <ActivityIndicator size="large" />;
}
if (errorMessage) {
return <Text>Unable to load users: {errorMessage}</Text>;
}
if (users.length === 0) {
return <Text>No users found.</Text>;
}
return (
<View>
<Text>Users</Text>
<FlatList
data={users}
keyExtractor={item => String(item.id)}
renderItem={({ item }) => (
<Text>{item.name}</Text>
)}
/>
</View>
);
}
13.2 Explanation of the Component
useState() stores the users, loading state, and error message.
useEffect() starts the request when the component mounts.
- The inner async function performs the request because the effect callback itself should not be declared
async.
response.ok checks whether the HTTP status indicates success.
try...catch handles errors during the request and response processing.
finally ends the loading state after success or failure.
- The cleanup flag prevents state updates from this request after the component has unmounted.
FlatList renders the user collection with a stable key for each item.
For more complex screens, consider request cancellation with AbortController when supported by the runtime and the request implementation. A cancellation-aware design can help prevent unnecessary work when screens change or users initiate a newer request.
14. Handling POST Requests
Applications often need to send information to a server, such as login details, profile updates, contact forms, or order information. Fetch can send a POST request with a request body.
14.1 Example: Sending JSON Data
async function createPost() {
const postData = {
title: "React Native Learning",
body: "Practising asynchronous JavaScript",
userId: 1
};
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(postData)
}
);
if (!response.ok) {
throw new Error(`Unable to create post: ${response.status}`);
}
return response.json();
}
createPost()
.then(data => console.log(data))
.catch(error => console.error(error.message));
14.2 Important Points
- Use the appropriate HTTP method for the operation.
- Set the content type when sending JSON.
- Convert JavaScript objects to JSON with
JSON.stringify().
- Check the response status before treating the request as successful.
- Handle validation errors returned by the server.
- Never store passwords or secret API credentials directly in source code.
- Use HTTPS for production requests involving sensitive information.
15. Executing Multiple Asynchronous Operations
Sometimes an application must load several independent resources, such as user details and product categories. JavaScript provides Promise utilities that help coordinate multiple asynchronous operations.
15.1 Promise.all()
Promise.all() fulfills when all supplied Promises fulfill. It rejects if any input Promise rejects. It is useful when all results are needed and the operations can run independently.
async function loadDashboard() {
const [usersResponse, postsResponse] = await Promise.all([
fetch("https://jsonplaceholder.typicode.com/users"),
fetch("https://jsonplaceholder.typicode.com/posts")
]);
if (!usersResponse.ok || !postsResponse.ok) {
throw new Error("One or more requests failed");
}
const [users, posts] = await Promise.all([
usersResponse.json(),
postsResponse.json()
]);
return { users, posts };
}
loadDashboard()
.then(data => console.log(data.users.length, data.posts.length))
.catch(error => console.error(error.message));
The requests start independently. The code then checks both HTTP responses and reads both response bodies. If either request fails at the Promise level, the combined operation rejects.
15.2 Promise.allSettled()
Promise.allSettled() waits for every supplied Promise to settle and returns the outcome of each one. It is useful when one failed operation should not prevent the application from inspecting the results of the others.
const tasks = [
Promise.resolve("Profile loaded"),
Promise.reject(new Error("Notifications unavailable")),
Promise.resolve("Settings loaded")
];
Promise.allSettled(tasks).then(results => {
results.forEach(result => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.log("Failure:", result.reason.message);
}
});
});
15.3 Promise.race()
Promise.race() settles with the outcome of the first input Promise that settles. It is sometimes used to implement time limits, although a timeout race alone does not cancel the underlying operation.
const fastOperation = new Promise(resolve => {
setTimeout(() => resolve("Fast operation completed"), 500);
});
const slowOperation = new Promise(resolve => {
setTimeout(() => resolve("Slow operation completed"), 1500);
});
Promise.race([fastOperation, slowOperation])
.then(result => console.log(result));
Typical output:
Fast operation completed
15.4 Promise.any()
Promise.any() fulfills as soon as one input Promise fulfills. If every input Promise rejects, it rejects with an AggregateError.
Promise.any([
Promise.reject(new Error("First source failed")),
Promise.resolve("Second source succeeded"),
Promise.resolve("Third source succeeded")
]).then(result => {
console.log(result);
}).catch(error => {
console.error(error);
});
Output:
Second source succeeded
15.5 Sequential vs. Concurrent Operations
| Approach |
When to Use It |
Example |
Sequential await |
When the next operation depends on the previous result. |
Fetch a user, then use the user ID to fetch their orders. |
Promise.all() |
When every independent result is required. |
Load profile data and categories at the same time. |
Promise.allSettled() |
When each operation's outcome should be inspected independently. |
Load several optional dashboard panels. |
Promise.race() |
When the first settled outcome determines the next step. |
Race a request against a timeout signal. |
Promise.any() |
When the first successful result is sufficient. |
Try multiple independent data sources. |
16. Common Mistakes in Async/Await and Promises
16.1 Forgetting await
async function getUsers() {
const users = fetch("https://jsonplaceholder.typicode.com/users");
console.log(users);
}
The variable users holds a Promise, not the parsed user data. Correct the code by awaiting the request and reading the response body.
async function getUsers() {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const users = await response.json();
console.log(users);
}
16.2 Assuming fetch Rejects for Every HTTP Error
const response = await fetch("https://example.com/missing-page");
// A 404 response does not normally reject fetch by itself.
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
Check the status explicitly. A request may successfully receive an HTTP response whose status indicates failure.
16.3 Forgetting to Catch a Rejection
async function loadProfile() {
const response = await fetch("https://example.com/profile");
return response.json();
}
loadProfile();
If the request fails, the returned Promise may reject without a handler. Handle the error at the calling level or inside the function if the function can meaningfully recover.
loadProfile().catch(error => {
console.error("Profile loading failed:", error.message);
});
16.4 Swallowing Errors Accidentally
async function loadProfile() {
try {
const response = await fetch("https://example.com/profile");
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(error.message);
// Returning nothing here converts the failure into a successful
// Promise fulfilled with undefined.
}
}
If the caller needs to know that the operation failed, rethrow the error after logging it or return an explicitly documented fallback value.
async function loadProfile() {
try {
const response = await fetch("https://example.com/profile");
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Profile loading failed:", error.message);
throw error;
}
}
16.5 Awaiting Independent Requests One by One
// These requests run sequentially.
const users = await fetchUsers();
const categories = await fetchCategories();
If the operations are independent, they can often run concurrently.
const [users, categories] = await Promise.all([
fetchUsers(),
fetchCategories()
]);
Only use concurrency when the operations do not depend on each other's results and the application can handle the combined failure behavior.
16.6 Declaring useEffect as Async
// Avoid this pattern:
useEffect(async () => {
const response = await fetch(url);
}, []);
The effect callback should return either nothing or a cleanup function, not a Promise. Define an async function inside the effect and call it instead.
useEffect(() => {
async function loadData() {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
// Update component state here.
} catch (error) {
// Handle the error here.
}
}
loadData();
}, []);
16.7 Updating State After a Screen Unmounts
An asynchronous request may finish after the user has left a screen. Depending on the implementation, updating state after the component is no longer active can cause unnecessary work or stale behavior.
Use an appropriate cleanup strategy, such as an AbortController where supported, or a cleanup flag to ignore results after unmounting. For requests that can be superseded by newer requests, also consider preventing older results from overwriting newer data.
17. Best Practices for React Native Applications
- Use
async/await when it makes a sequence of dependent operations easier to read.
- Use Promise chaining when it clearly expresses the workflow.
- Handle rejected Promises instead of leaving them unobserved.
- Check
response.ok before processing an HTTP response as successful.
- Keep network requests and data-processing logic in reusable functions when appropriate.
- Show loading indicators while important requests are pending.
- Display user-friendly messages when operations fail.
- Keep technical error details in development logs, and avoid exposing sensitive information to users.
- Use
Promise.all() for independent operations when every result is required.
- Use
Promise.allSettled() when each operation should be handled independently.
- Validate API response data before using it in the interface.
- Handle empty responses and unexpected data formats.
- Avoid unnecessary Promise wrappers.
- Use cleanup or cancellation strategies for requests that may outlive a screen.
- Keep authentication tokens and sensitive credentials out of hard-coded source files.
- Test success, failure, timeout, empty-data, and offline scenarios.
18. Practical Exercise 1: Create a Promise
Objective: Create a Promise that resolves when a score is at least 50 and rejects otherwise.
function checkScore(score) {
return new Promise((resolve, reject) => {
if (score >= 50) {
resolve("You passed");
} else {
reject(new Error("You failed"));
}
});
}
checkScore(75)
.then(message => console.log(message))
.catch(error => console.error(error.message));
Expected output:
You passed
Practice task: Test the function with a score below 50 and handle the rejection.
19. Practical Exercise 2: Convert a Function to Async/Await
Objective: Use async/await to retrieve and display a result from a Promise.
function getMessage() {
return Promise.resolve("Learning asynchronous JavaScript");
}
async function displayMessage() {
const message = await getMessage();
console.log(message);
}
displayMessage();
Expected output:
Learning asynchronous JavaScript
Practice task: Change the function to return a rejected Promise and handle the error using try...catch.
20. Practical Exercise 3: Fetch and Display API Data
Objective: Fetch a list of users, check the HTTP response, parse the JSON, and handle errors.
async function fetchUsers() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const users = await response.json();
console.log("Users loaded:", users.length);
return users;
} catch (error) {
console.error("Failed to fetch users:", error.message);
throw error;
}
}
fetchUsers().catch(() => {
// The failure has already been logged by fetchUsers().
});
Practice task: Modify the code to return only each user's name and email address.
21. Mini Project: React Native API Data Screen
Project objective: Build a React Native screen that retrieves data from an API, displays a loading indicator, renders results, and shows an error message when the request fails.
21.1 Project Features
- Fetch user information asynchronously.
- Show a loading indicator while the request is running.
- Check the HTTP response status.
- Parse JSON data.
- Display a list of users.
- Handle network and response-processing errors.
- Show a message when no users are available.
- Use a cleanup flag to avoid updating state after the component unmounts.
21.2 Example Implementation
import React, { useEffect, useState } from "react";
import {
View,
Text,
FlatList,
ActivityIndicator,
StyleSheet
} from "react-native";
export default function UsersScreen() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState("");
useEffect(() => {
let isActive = true;
async function loadUsers() {
try {
setLoading(true);
setErrorMessage("");
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error("Unexpected response format");
}
if (isActive) {
setUsers(data);
}
} catch (error) {
if (isActive) {
setErrorMessage(
error instanceof Error
? error.message
: "An unexpected error occurred"
);
}
} finally {
if (isActive) {
setLoading(false);
}
}
}
loadUsers();
return () => {
isActive = false;
};
}, []);
if (loading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" />
<Text>Loading users...</Text>
</View>
);
}
if (errorMessage) {
return (
<View style={styles.center}>
<Text>Unable to load users.</Text>
<Text>{errorMessage}</Text>
</View>
);
}
if (users.length === 0) {
return (
<View style={styles.center}>
<Text>No users found.</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.heading}>User List</Text>
<FlatList
data={users}
keyExtractor={item => String(item.id)}
renderItem={({ item }) => (
<View style={styles.card}>
<Text style={styles.name}>{item.name}</Text>
<Text>{item.email}</Text>
</View>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16
},
center: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 16
},
heading: {
fontSize: 24,
fontWeight: "bold",
marginBottom: 16
},
card: {
padding: 12,
marginBottom: 10,
borderWidth: 1,
borderColor: "#cccccc",
borderRadius: 8
},
name: {
fontSize: 18,
fontWeight: "bold"
}
});
21.3 Project Explanation
useState() stores the fetched users, loading status, and error message.
useEffect() starts the request when the screen component mounts.
- The inner
loadUsers() function uses async/await to manage the asynchronous request.
- The response status is checked before the body is processed.
- The JSON response is validated to ensure it is an array.
- The
catch block stores a useful error message for the screen.
- The
finally block ends the loading state after success or failure.
- The cleanup flag prevents this request from updating state after unmounting.
FlatList renders the collection efficiently using unique IDs as keys.
22. Interview Questions and Answers
Q1. What is a Promise in JavaScript?
A Promise represents the eventual completion or failure of an asynchronous operation and provides methods for handling its outcome.
Q2. What are the three states of a Promise?
The three states are pending, fulfilled, and rejected.
Q3. What is the purpose of resolve()?
resolve() fulfills a Promise with a value or makes it adopt the state of another Promise or thenable.
Q4. What is the purpose of reject()?
reject() rejects a Promise with a reason, commonly an Error object.
Q5. What is the difference between then() and catch()?
then() registers a fulfillment handler and can also accept a rejection handler as its second argument. catch() registers a rejection handler and is commonly used for error handling in a Promise chain.
Q6. What does finally() do?
finally() runs after the Promise settles, whether it fulfills or rejects. It is useful for cleanup operations.
Q7. What is an async function?
An async function is declared with the async keyword and always returns a Promise.
Q8. What does await do?
await suspends the surrounding async function until a Promise settles, returning its fulfillment value or throwing its rejection reason.
Q9. Can await be used outside an async function?
In JavaScript modules that support top-level await, it can be used at the module's top level. Otherwise, it is normally used inside an async function.
Q10. How do you handle errors in async/await?
Use try...catch around awaited operations and use finally for cleanup when necessary.
Q11. Does fetch() reject when the server returns a 404 response?
Normally, no. Fetch fulfills with a Response object for HTTP responses such as 404. Check response.ok or the status code to detect unsuccessful HTTP responses.
Q12. What is Promise.all()?
Promise.all() waits for all supplied Promises to fulfill and returns their results in input order. It rejects if any input Promise rejects.
Q13. What is Promise.allSettled()?
Promise.allSettled() waits for every input Promise to settle and returns an outcome record for each Promise.
Q14. Why is async/await useful in React Native?
It makes asynchronous workflows such as API requests, data loading, and saving information easier to read and maintain.
Q15. Why should asynchronous errors be handled?
Handling errors allows the application to show helpful messages, avoid unhandled rejections, provide recovery behavior, and help developers diagnose problems.
Q16. Why should a useEffect callback not directly be async?
A React effect callback should return nothing or a cleanup function. An async function returns a Promise, so define and call an async function inside the effect instead.
Q17. What is the difference between sequential and concurrent operations?
Sequential operations wait for earlier results before starting dependent steps. Concurrent operations allow independent tasks to progress at the same time, often coordinated with Promise.all().
Q18. What is a common danger of catching an error without rethrowing it?
If the catch block returns normally without rethrowing or returning an explicitly intended fallback, the async function may fulfill with undefined or another fallback value instead of informing its caller that the operation failed.
23. Quick Revision Checklist
- Understand synchronous and asynchronous execution.
- Know the pending, fulfilled, and rejected Promise states.
- Use
then() to process successful Promise results.
- Use
catch() to handle rejected Promises.
- Use
finally() for cleanup after settlement.
- Remember that async functions always return Promises.
- Use
await when a later step depends on an asynchronous result.
- Use
try...catch to handle errors from awaited operations.
- Check
response.ok when using Fetch.
- Handle network failures, HTTP errors, and invalid response data.
- Manage loading, success, empty, and error states in React Native.
- Use
Promise.all() for independent operations when every result is required.
- Use
Promise.allSettled() when each outcome should be handled independently.
- Do not leave rejected Promises unhandled.
- Use cleanup or cancellation strategies when requests can outlive a screen.
24. Recommended Learning Resources
25. Conclusion
Promises, Async/Await, and Error Handling are essential concepts for building reliable JavaScript and React Native applications. Promises represent the outcomes of asynchronous operations, while async/await provides a readable way to work with them. Error-handling techniques help applications respond safely when network requests, data processing, or other operations fail.
React Native developers use these concepts when retrieving API data, submitting forms, loading profiles, saving information, and coordinating multiple requests. By practising Promise chaining, try...catch, HTTP status checks, loading states, and concurrent operations, learners can create mobile applications that are easier to maintain and more resilient to failures.
To continue learning, visit React Native Training at JustAcademy or register through the course demo registration page.